/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.views.toolbar;

import android.content.Context;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;

import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.DraweeHolder;
import com.facebook.drawee.view.MultiDraweeHolder;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.image.QualityInfo;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.PixelUtil;

import javax.annotation.Nullable;

/**
 * Custom implementation of the {@link Toolbar} widget that adds support for remote images in logo
 * and navigationIcon using fresco.
 */
public class ReactToolbar extends Toolbar {

  private static final String PROP_ACTION_ICON = "icon";
  private static final String PROP_ACTION_SHOW = "show";
  private static final String PROP_ACTION_SHOW_WITH_TEXT = "showWithText";
  private static final String PROP_ACTION_TITLE = "title";

  private static final String PROP_ICON_URI = "uri";
  private static final String PROP_ICON_WIDTH = "width";
  private static final String PROP_ICON_HEIGHT = "height";

  private final DraweeHolder mLogoHolder;
  private final DraweeHolder mNavIconHolder;
  private final DraweeHolder mOverflowIconHolder;
  private final MultiDraweeHolder<GenericDraweeHierarchy> mActionsHolder =
          new MultiDraweeHolder<>();

  private IconControllerListener mLogoControllerListener;
  private IconControllerListener mNavIconControllerListener;
  private IconControllerListener mOverflowIconControllerListener;

  /**
   * Attaches specific icon width & height to a BaseControllerListener which will be used to
   * create the Drawable
   */
  private abstract class IconControllerListener extends BaseControllerListener<ImageInfo> {

    private final DraweeHolder mHolder;

    private IconImageInfo mIconImageInfo;

    public IconControllerListener(DraweeHolder holder) {
      mHolder = holder;
    }

    public void setIconImageInfo(IconImageInfo iconImageInfo) {
      mIconImageInfo = iconImageInfo;
    }

    @Override
    public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
      super.onFinalImageSet(id, imageInfo, animatable);

      final ImageInfo info = mIconImageInfo != null ? mIconImageInfo : imageInfo;
      setDrawable(new DrawableWithIntrinsicSize(mHolder.getTopLevelDrawable(), info));
    }

    protected abstract void setDrawable(Drawable d);

  }

  private class ActionIconControllerListener extends IconControllerListener {
    private final MenuItem mItem;

    ActionIconControllerListener(MenuItem item, DraweeHolder holder) {
      super(holder);
      mItem = item;
    }

    @Override
    protected void setDrawable(Drawable d) {
      mItem.setIcon(d);
    }
  }

  /**
   * Simple implementation of ImageInfo, only providing width & height
   */
  private static class IconImageInfo implements ImageInfo {

    private int mWidth;
    private int mHeight;

    public IconImageInfo(int width, int height) {
      mWidth = width;
      mHeight = height;
    }

    @Override
    public int getWidth() {
      return mWidth;
    }

    @Override
    public int getHeight() {
      return mHeight;
    }

    @Override
    public QualityInfo getQualityInfo() {
      return null;
    }

  }

  public ReactToolbar(Context context) {
    super(context);

    mLogoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
    mNavIconHolder = DraweeHolder.create(createDraweeHierarchy(), context);
    mOverflowIconHolder = DraweeHolder.create(createDraweeHierarchy(), context);

    mLogoControllerListener = new IconControllerListener(mLogoHolder) {
      @Override
      protected void setDrawable(Drawable d) {
        setLogo(d);
      }
    };

    mNavIconControllerListener = new IconControllerListener(mNavIconHolder) {
      @Override
      protected void setDrawable(Drawable d) {
        setNavigationIcon(d);
      }
    };

    mOverflowIconControllerListener = new IconControllerListener(mOverflowIconHolder) {
      @Override
      protected void setDrawable(Drawable d) {
        setOverflowIcon(d);
      }
    };

  }

  private final Runnable mLayoutRunnable = new Runnable() {
    @Override
    public void run() {
      measure(
          MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
      layout(getLeft(), getTop(), getRight(), getBottom());
    }
  };

  @Override
  public void requestLayout() {
    super.requestLayout();

    // The toolbar relies on a measure + layout pass happening after it calls requestLayout().
    // Without this, certain calls (e.g. setLogo) only take effect after a second invalidation.
    post(mLayoutRunnable);
  }

  @Override
  public void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    detachDraweeHolders();
  }

  @Override
  public void onStartTemporaryDetach() {
    super.onStartTemporaryDetach();
    detachDraweeHolders();
  }

  @Override
  public void onAttachedToWindow() {
    super.onAttachedToWindow();
    attachDraweeHolders();
  }

  @Override
  public void onFinishTemporaryDetach() {
    super.onFinishTemporaryDetach();
    attachDraweeHolders();
  }

  private void detachDraweeHolders() {
    mLogoHolder.onDetach();
    mNavIconHolder.onDetach();
    mOverflowIconHolder.onDetach();
    mActionsHolder.onDetach();
  }

  private void attachDraweeHolders() {
    mLogoHolder.onAttach();
    mNavIconHolder.onAttach();
    mOverflowIconHolder.onAttach();
    mActionsHolder.onAttach();
  }

  /* package */ void setLogoSource(@Nullable ReadableMap source) {
    setIconSource(source, mLogoControllerListener, mLogoHolder);
  }

  /* package */ void setNavIconSource(@Nullable ReadableMap source) {
    setIconSource(source, mNavIconControllerListener, mNavIconHolder);
  }

  /* package */ void setOverflowIconSource(@Nullable ReadableMap source) {
    setIconSource(source, mOverflowIconControllerListener, mOverflowIconHolder);
  }

  /* package */ void setActions(@Nullable ReadableArray actions) {
    Menu menu = getMenu();
    menu.clear();
    mActionsHolder.clear();
    if (actions != null) {
      for (int i = 0; i < actions.size(); i++) {
        ReadableMap action = actions.getMap(i);

        MenuItem item = menu.add(Menu.NONE, Menu.NONE, i, action.getString(PROP_ACTION_TITLE));

        if (action.hasKey(PROP_ACTION_ICON)) {
          setMenuItemIcon(item, action.getMap(PROP_ACTION_ICON));
        }

        int showAsAction = action.hasKey(PROP_ACTION_SHOW)
            ? action.getInt(PROP_ACTION_SHOW)
            : MenuItem.SHOW_AS_ACTION_NEVER;
        if (action.hasKey(PROP_ACTION_SHOW_WITH_TEXT) &&
            action.getBoolean(PROP_ACTION_SHOW_WITH_TEXT)) {
          showAsAction = showAsAction | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
        }
        item.setShowAsAction(showAsAction);
      }
    }
  }

  private void setMenuItemIcon(final MenuItem item, ReadableMap iconSource) {

    DraweeHolder<GenericDraweeHierarchy> holder =
            DraweeHolder.create(createDraweeHierarchy(), getContext());
    ActionIconControllerListener controllerListener = new ActionIconControllerListener(item, holder);
    controllerListener.setIconImageInfo(getIconImageInfo(iconSource));

    setIconSource(iconSource, controllerListener, holder);

    mActionsHolder.add(holder);

  }

  /**
   * Sets an icon for a specific icon source. If the uri indicates an icon
   * to be somewhere remote (http/https) or on the local filesystem, it uses fresco to load it.
   * Otherwise it loads the Drawable from the Resources and directly returns it via a callback
   */
  private void setIconSource(ReadableMap source, IconControllerListener controllerListener, DraweeHolder holder) {

    String uri = source != null ? source.getString(PROP_ICON_URI) : null;

    if (uri == null) {
      controllerListener.setIconImageInfo(null);
      controllerListener.setDrawable(null);
    } else if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("file://")) {
      controllerListener.setIconImageInfo(getIconImageInfo(source));
      DraweeController controller = Fresco.newDraweeControllerBuilder()
              .setUri(Uri.parse(uri))
              .setControllerListener(controllerListener)
              .setOldController(holder.getController())
              .build();
      holder.setController(controller);
    } else {
      controllerListener.setDrawable(getDrawableByName(uri));
    }

  }

  private GenericDraweeHierarchy createDraweeHierarchy() {
    return new GenericDraweeHierarchyBuilder(getResources())
        .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
        .setFadeDuration(0)
        .build();
  }

  private int getDrawableResourceByName(String name) {
    return getResources().getIdentifier(
        name,
        "drawable",
        getContext().getPackageName());
  }

  private Drawable getDrawableByName(String name) {
    int drawableResId = getDrawableResourceByName(name);
    if (drawableResId != 0) {
      return getResources().getDrawable(getDrawableResourceByName(name));
    } else {
      return null;
    }
  }

  private IconImageInfo getIconImageInfo(ReadableMap source) {
    if (source.hasKey(PROP_ICON_WIDTH) && source.hasKey(PROP_ICON_HEIGHT)) {
      final int width = Math.round(PixelUtil.toPixelFromDIP(source.getInt(PROP_ICON_WIDTH)));
      final int height = Math.round(PixelUtil.toPixelFromDIP(source.getInt(PROP_ICON_HEIGHT)));
      return new IconImageInfo(width, height);
    } else {
      return null;
    }
  }

}
